package org.internna.ossmoney.model.support;

import java.util.Map;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TreeMap;
import java.util.Calendar;
import java.util.ArrayList;
import java.text.NumberFormat;
import java.math.BigDecimal;
import org.internna.ossmoney.model.Category;
import org.internna.ossmoney.util.DateUtils;
import org.internna.ossmoney.model.Subcategory;
import org.springframework.util.CollectionUtils;
import org.springframework.context.MessageSource;

public final class InflowOutflow {

	private final Locale locale;
	private final String currency;
	private final Interval interval;
	private final MessageSource messageSource;
	private final Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> inflow, outflow;

	public InflowOutflow(Interval interval, String currency, Locale locale, MessageSource messageSource) {
		this.locale = locale;
		this.currency = currency;
		this.interval = interval;
		this.messageSource = messageSource;
		this.inflow = new TreeMap<Date, Map<Category,Map<Subcategory,BigDecimal>>>();
		this.outflow = new TreeMap<Date, Map<Category,Map<Subcategory,BigDecimal>>>();
	}

	public String getCurrency() {
		return currency;
	}

	protected Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> getInflow() {
		return inflow;
	}

	protected Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> getOutflow() {
		return outflow;
	}

	public Map<Category, Map<Subcategory, BigDecimal>> getInflow(Date date) {
		return get(date, true);
	}

	public Map<Category, Map<Subcategory, BigDecimal>> getOutflow(Date date) {
		return get(date, false);
	}

	protected Map<Category, Map<Subcategory, BigDecimal>> get(Date date, boolean in) {
		Date month = DateUtils.getMonthStartDate(date);
		Map<Category, Map<Subcategory, BigDecimal>> data = in ? inflow.get(month) : outflow.get(month);
		if (data == null) {
			data = new TreeMap<Category, Map<Subcategory, BigDecimal>>();
			(in ? inflow : outflow).put(month, data);
		}
		return data;
	}

	public final String getAsJSONString() {
		NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
		StringBuilder builder = new StringBuilder("{\"identifier\": \"id\", \"label\": \"label\", \"items\": [");
		NameValuePair<String, List<BigDecimal>> inflows = asJSON(formatter, 1, "reporting.json.inflow", inflow);
		NameValuePair<String, List<BigDecimal>> outflows = asJSON(formatter, 2, "reporting.json.outflow", outflow);
		builder.append(inflows.getKey()).append(",");
		builder.append(outflows.getKey()).append(",");
		builder.append(totals(formatter, inflows.getValue(), outflows.getValue()));
		return builder.append("]}").toString();
	}

	protected StringBuilder totals(NumberFormat formatter, List<BigDecimal> inflows, List<BigDecimal> outflows) {
		StringBuilder builder = new StringBuilder();
		builder.append("{\"id\": 3, \"label\": \"<b>")
			.append(messageSource.getMessage("reporting.json.totals", null, "Totals", locale))
			.append("</b>\", ");
		BigDecimal total = BigDecimal.ZERO;
		for (int index = 0; index < inflows.size(); index++) {
			BigDecimal month = inflows.get(index).abs().add(outflows.get(index).abs().negate());
			total = total.add(month);
			builder.append("\"totalMonth" + index + "\": \"").append(formatAmountWithNegatives(formatter, month)).append("\", ");
		}
		builder.append("\"total\": \"").append(formatAmountWithNegatives(formatter, total)).append("\"}");
		return builder;
	}

	protected NameValuePair<String, List<BigDecimal>> asJSON(NumberFormat formatter, int id, String i18n, Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> data) {
		StringBuilder builder = new StringBuilder();
		builder.append("{\"id\": ").append(id).append(", \"label\": \"")
			.append(messageSource.getMessage(i18n, null, "Flow", locale))
			.append("\", ");
		BigDecimal total = BigDecimal.ZERO;
		List<BigDecimal> inflowTotals = new ArrayList<BigDecimal>();
		for (int index = 0; index < interval.getNumberOfMonths(); index++) {
			BigDecimal month = calculateMonthTotal(index, data);
			inflowTotals.add(month);
			total = total.add(month);
			builder.append("\"totalMonth" + index + "\": \"").append(formatAmount(formatter, month)).append("\", ");
		}
		builder.append("\"total\": \"").append(formatAmount(formatter, total)).append("\", \"categories\": [")
			.append(categoriesAsJSON(formatter, revert(data))).append("]}");
		return new NameValuePair<String, List<BigDecimal>>(builder.toString(), inflowTotals);
	}

	protected Map<Category, Map<Subcategory, Map<Date, BigDecimal>>> revert(Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> data) {
		Map<Category, Map<Subcategory, Map<Date, BigDecimal>>> reverted = new TreeMap<Category, Map<Subcategory, Map<Date, BigDecimal>>>();
		for (Map.Entry<Date, Map<Category, Map<Subcategory, BigDecimal>>> entry : data.entrySet()) {
			for (Map.Entry<Category, Map<Subcategory, BigDecimal>> innerEntry : entry.getValue().entrySet()) {
				Map<Subcategory, Map<Date, BigDecimal>> values = getOrCreateCategory(reverted, innerEntry.getKey());
				for (Map.Entry<Subcategory, BigDecimal> finalEntry : innerEntry.getValue().entrySet()) {
					Map<Date, BigDecimal> dateTotals = getOrCreateSubcategory(values, finalEntry.getKey());
					BigDecimal subtotal = dateTotals.get(entry.getKey());
					if (subtotal == null) {
						subtotal = BigDecimal.ZERO;
					}
					subtotal = subtotal.add(finalEntry.getValue());
					dateTotals.put(entry.getKey(), subtotal);
				}
			}
		}
		return reverted;
	}

	protected Map<Subcategory, Map<Date, BigDecimal>> getOrCreateCategory(Map<Category, Map<Subcategory, Map<Date, BigDecimal>>> reverted, Category category) {
		Map<Subcategory, Map<Date, BigDecimal>> data = reverted.get(category);
		if (data == null) {
			data = new TreeMap<Subcategory, Map<Date, BigDecimal>>();
			reverted.put(category, data);
		}
		return data;
	}

	protected Map<Date, BigDecimal> getOrCreateSubcategory(Map<Subcategory, Map<Date, BigDecimal>> values, Subcategory subcategory) {
		Map<Date, BigDecimal> data = values.get(subcategory);
		if (data == null) {
			data = new TreeMap<Date, BigDecimal>();
			values.put(subcategory, data);
		}
		return data;
	}

	protected String categoriesAsJSON(NumberFormat formatter, Map<Category, Map<Subcategory, Map<Date, BigDecimal>>> reverted) {
		StringBuilder builder = new StringBuilder();
		for (Map.Entry<Category, Map<Subcategory, Map<Date, BigDecimal>>> entry : reverted.entrySet()) {
			builder.append(",{\"id\": 999").append(entry.getKey().getId()).append(", \"label\": \"")
				.append(messageSource.getMessage(entry.getKey().getCategory(), null, entry.getKey().getCategory(), locale)).append("\", ");
			BigDecimal total = BigDecimal.ZERO;
			for (int index = 0; index < interval.getNumberOfMonths(); index++) {
				BigDecimal category = calculateCategoryDateTotal(index, entry.getValue());
				total = total.add(category);
				builder.append("\"totalMonth" + index + "\": \"").append(formatAmount(formatter, category)).append("\", ");
			}
			builder.append("\"total\": \"").append(formatAmount(formatter, total)).append("\", \"subcategories\": [")
				.append(subcategoriesAsJSON(formatter, entry.getValue())).append("]}");
		}
		return builder.length() > 0 ? builder.substring(1) : builder.toString();
	}

	protected BigDecimal calculateCategoryDateTotal(int month, Map<Subcategory, Map<Date, BigDecimal>> data) {
		BigDecimal total = BigDecimal.ZERO;
		Date monthDate = getMonth(month);
		for (Map<Date, BigDecimal> value : data.values()) {
			for (Map.Entry<Date, BigDecimal> dateAmount: value.entrySet()) {
				if (dateAmount.getKey().equals(monthDate)) {
					total = total.add(dateAmount.getValue().abs());
				}
			}
		}
		return total;
	}

	protected String subcategoriesAsJSON(NumberFormat formatter, Map<Subcategory, Map<Date, BigDecimal>> data) {
		StringBuilder builder = new StringBuilder();
		for (Map.Entry<Subcategory, Map<Date, BigDecimal>> entry : data.entrySet()) {
			builder.append(",{\"id\": ").append(entry.getKey().getId()).append(", \"label\": \"")
			.append(messageSource.getMessage(entry.getKey().getCategory(), null, entry.getKey().getCategory(), locale)).append("\", ");
			BigDecimal total = BigDecimal.ZERO;
			for (int index = 0; index < interval.getNumberOfMonths(); index++) {
				BigDecimal subcategory = calculateSubcategoryDateTotal(index, entry.getValue());
				total = total.add(subcategory);
				builder.append("\"totalMonth" + index + "\": \"").append(formatAmount(formatter, subcategory)).append("\", ");
			}
			builder.append("\"total\": \"").append(formatAmount(formatter, total)).append("\"}");
		}
		return builder.length() > 0 ? builder.substring(1) : builder.toString();
	}

	protected BigDecimal calculateSubcategoryDateTotal(int month, Map<Date, BigDecimal> data) {
		Date monthDate = getMonth(month);
		return data.containsKey(monthDate) ? data.get(monthDate) : BigDecimal.ZERO;
	}

	protected BigDecimal calculateMonthTotal(int month, Map<Date, Map<Category, Map<Subcategory, BigDecimal>>> data) {
		BigDecimal total = BigDecimal.ZERO;
		Map<Category, Map<Subcategory, BigDecimal>> categoryData = data.get(getMonth(month));
		if (!CollectionUtils.isEmpty(categoryData)) {
			for (Map<Subcategory, BigDecimal> entry : categoryData.values()) {
				total = total.add(calculateCategoryTotal(entry));
			}
		}
		return total;
	}

	protected BigDecimal calculateCategoryTotal(Map<Subcategory, BigDecimal> data) {
		BigDecimal total = BigDecimal.ZERO;
		if (!CollectionUtils.isEmpty(data)) {
			for (BigDecimal entry : data.values()) {
				total = total.add(entry.abs());
			}
		}
		return total;
	}

	protected Date getMonth(int months) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(DateUtils.getMonthStartDate(new Date()));
		calendar.add(Calendar.MONTH, months > 0 ? -1 * months : months);
		return calendar.getTime();
	}

	protected String formatAmount(final NumberFormat formatter, final BigDecimal amount) {
		return BigDecimal.ZERO.equals(amount) ? "" : formatter.format(amount.doubleValue());
	}

	protected String formatAmountWithNegatives(final NumberFormat formatter, final BigDecimal amount) {
		String formatted = "<b>" + formatAmount(formatter, amount) + "</b>";
		return BigDecimal.ZERO.compareTo(amount) > 0 ? "<span style='color:red'>" + formatted + "</span>" : formatted;
	}

}
